import { Injectable } from '@nestjs/common';
import { In, Repository } from 'typeorm';
import { EQModel } from './eq.model';
import { InjectRepository } from '@nestjs/typeorm';
import { EQDto, EQGetFiltersBySegmentFreqDto, EQGetFiltersByTempEQDto, EQIndexListDto, EQIndexOpenListDto, EQShowOpenDto, EQTop10ListDto, EQUpdateDto } from './eq.dto';
import { last, uniqBy } from 'lodash';
import { EvaluationModel } from '../evaluation/evaluation.model';
import { BizFailedException } from '@/common/biz-failed/biz-failed.exception';
import { BizFailedCodeEnum } from '@/common/biz-failed/biz-failed.enum';
import { EvaluationDataModel } from '../evaluation-data/evaluation-data.model';
import { csvStrToDataSet } from 'common/utils/utils';
import { euclideanDistance, scoreCurveVector } from './util';
import { EvaluationService } from '../evaluation/evaluation.service';
import { EvaluationDataService } from '../evaluation-data/evaluation-data.service';
import { getFrequencyPoints } from 'common/utils/eq-filter/utils';
import { FilterOptions } from 'common/utils/eq-filter/types';
import dayjs from 'dayjs';
import { EarBudsWearingMethod, EQ_AUDIO_FILE_CLEAN_DAYS, FileGroup } from 'common/utils/data';
import { FileService } from '../file/file.service';
import { Cron } from '@nestjs/schedule';
import { LoggerService } from '@/common/logger/logger.service';
import { EvaluationSpecificationModel } from '../evaluation-specification/evaluation-specification.model';
import { ConfigService } from '@nestjs/config';
import path from 'path';
import { customAlphabet } from 'nanoid';
import fsExtra from 'fs-extra';
import { exec } from 'child_process';

@Injectable()
export class EQService {
  tempFilePath: string;
  useConv: boolean;

  constructor(
    @InjectRepository(EQModel)
    private readonly eqModelRepository: Repository<EQModel>,

    @InjectRepository(EvaluationModel)
    private readonly evaluationModelRepository: Repository<EvaluationModel>,

    @InjectRepository(EvaluationDataModel)
    private readonly evaluationDataModelRepository: Repository<EvaluationDataModel>,

    @InjectRepository(EvaluationSpecificationModel)
    private readonly evaluationSpecificationModelRepository: Repository<EvaluationSpecificationModel>,

    private readonly evaluationService: EvaluationService,
    private readonly evaluationDataService: EvaluationDataService,
    private readonly loggerService: LoggerService,
    private readonly fileService: FileService,
    private readonly configService: ConfigService,
  ) {
    this.tempFilePath = this.configService.get<string>('TEMP_FILE_PATH')!;
    this.useConv = this.configService.get<string>('USE_CONV') === '1'
  }

  async cleanOssFile() {
    const clean = async () => {
      const cleanOssList = await this.eqModelRepository
        .createQueryBuilder('eq')
        .where('eq.updateTime <= :date', {
          date: dayjs()
            .subtract(EQ_AUDIO_FILE_CLEAN_DAYS, 'd')
            .startOf('d')
            .format('YYYY-MM-DD HH:mm:ss'),
        })
        .andWhere('json_array_length(eq.audioFiles) > 0')
        .orderBy('eq.updateTime', 'ASC')
        .addOrderBy('eq.id', 'ASC')
        .limit(500)
        .getMany();
      if (cleanOssList.length) {
        this.loggerService.log(
          `此轮以下数据需要清除对应文件，共${cleanOssList.length}条数据`,
        );
        this.loggerService.log(
          cleanOssList.map((item) => {
            return {
              uuid: item.uuid,
              audioFiles: item.audioFiles,
              createTime: dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss'),
            };
          }),
        );
        const fileList = Array.from(
          new Set(cleanOssList.map((item) => item.audioFiles).flat()),
        ).filter((item) => item.startsWith(`${FileGroup.TEMP_EQ_AUDIO}/`));
        this.loggerService.log('此轮oss清除以下文件');
        this.loggerService.log(fileList);
        const res = await this.fileService.batchRemoveFile(fileList);
        this.loggerService.log('此轮oss清除结果');
        this.loggerService.log(res);
        await this.eqModelRepository.update(
          {
            uuid: In(cleanOssList.map((item) => item.uuid)),
          },
          {
            audioFiles: [],
          },
        );
        this.loggerService.log('此轮表数据修改完毕');
        await clean();
      } else {
        this.loggerService.log('已无需清理文件');
      }
    };

    this.loggerService.log('清理oss文件开始');
    await clean();
    this.loggerService.log('清理oss文件结束');
  }

  @Cron('01 00 * * *')
  async autoCleanOssFile() {
    if (
      process.env.NODE_APP_INSTANCE &&
      process.env.NODE_APP_INSTANCE !== '0'
    ) {
      return;
    }
    await this.cleanOssFile();
  }

  async index(query: EQIndexListDto, userUuid: string) {
    const queryBuilder = this.eqModelRepository.createQueryBuilder('eq');

    queryBuilder
      .select([
        'eq.uuid',
        'eq.evaluationUuid',
        'eq.dataGroup',
        'eq.name',
        'eq.desc',
        'eq.audioFiles',
        'eq.filters',
        'eq.createTime',
        'eq.updateTime',
        'eq.open',
      ])
      .andWhere(`(eq.userUuid = '${userUuid}')`)

    if (query.keyword) {
      queryBuilder.andWhere('(eq.name ILIKE :keyword)', {
        keyword: `%${query.keyword}%`,
      });
    }

    query.wrapQueryBuilder(queryBuilder, 'eq')

    return queryBuilder.getManyAndCount();
  }

  async indexOpen(query: EQIndexOpenListDto) {
    const queryBuilder = this.eqModelRepository.createQueryBuilder('eq');

    queryBuilder
      .leftJoinAndSelect('eq.evaluation', 'evaluation')
      .leftJoinAndSelect('eq.user', 'user')
      .leftJoinAndSelect('evaluation.brand', 'brand')
      .select([
        'eq.uuid',
        'eq.evaluationUuid',
        'eq.dataGroup',
        'eq.name',
        'eq.desc',
        'eq.audioFiles',
        'eq.filters',
        'eq.createTime',
        'eq.updateTime',
        'evaluation.id',
        'evaluation.title',
        'brand.id',
        'brand.title',
        'user.id',
        'user.uuid',
        'user.avatar',
        'user.name',
        'user.titleCertification',
      ])
      .andWhere(`(eq.open = 1)`)

    if (query.keyword) {
      queryBuilder.andWhere('(eq.name ILIKE :keyword)', {
        keyword: `%${query.keyword}%`,
      });
    }

    query.wrapQueryBuilder(queryBuilder, 'eq')

    return queryBuilder.getManyAndCount();
  }

  async show(uuid: string, userUuid: string) {
    const eq = await this.eqModelRepository.findOne({
      where: {
        uuid,
        userUuid,
      },
      select: {
        id: true,
        uuid: true,
        evaluationUuid: true,
        dataGroup: true,
        name: true,
        audioFiles: true,
        filters: true,
        createTime: true,
        updateTime: true,
        desc: true,
      },
    });

    if (!eq) {
      throw new BizFailedException(BizFailedCodeEnum.EQ_NOT_EXISTS);
    }

    const points = await this.getPointsByEq({
      evaluationUuid: eq.evaluationUuid,
      dataGroup: eq.dataGroup,
      filters: eq.filters as FilterOptions[],
    });

    return {
      ...eq,
      points,
    };
  }

  async showOpen(uuid: string, query: EQShowOpenDto) {
    const queryBuilder = this.eqModelRepository.createQueryBuilder('eq');

    queryBuilder
      .leftJoinAndSelect('eq.evaluation', 'evaluation')
      .leftJoinAndSelect('eq.user', 'user')
      .leftJoinAndSelect('evaluation.brand', 'brand')
      .select([
        'eq.uuid',
        'eq.evaluationUuid',
        'eq.dataGroup',
        'eq.name',
        'eq.desc',
        'eq.audioFiles',
        'eq.filters',
        'eq.createTime',
        'eq.updateTime',
        'evaluation.id',
        'evaluation.title',
        'brand.id',
        'brand.title',
        'user.id',
        'user.uuid',
        'user.avatar',
        'user.name',
        'user.titleCertification',
      ])
      .andWhere(`(eq.open = 1)`)
      .andWhere(`(eq.uuid = :uuid)`, { uuid })

    const eq = await queryBuilder.getOne();
    if (!eq) {
      throw new BizFailedException(BizFailedCodeEnum.EQ_NOT_EXISTS);
    }

    const points = await this.getPointsByEq({
      evaluationUuid: eq.evaluationUuid,
      dataGroup: eq.dataGroup,
      filters: eq.filters as FilterOptions[],
    });

    return {
      ...eq,
      ...(query.evaluationUuid && query.dataGroup ? {
        evaluationUuid: query.evaluationUuid,
        dataGroup: query.dataGroup,
        filters: await this.getFiltersByPoints({
          evaluationUuid: query.evaluationUuid,
          dataGroup: query.dataGroup,
          points,
        }),
      } : {}),
      points,
    }

  }

  async create(body: EQDto, userUuid: string) {
    const { evaluationUuid, dataGroup, audioFiles, filters, name, desc } = body;

    const [evaluation] = await Promise.all([
      this.evaluationModelRepository.findOne({
        where: {
          uuid: evaluationUuid,
        },
        relations: ['brand'],
      }),
    ]);

    if (!evaluation) {
      throw new BizFailedException(BizFailedCodeEnum.EVALUATION_NOT_EXISTS);
    }

    return this.eqModelRepository.save({
      evaluationUuid,
      dataGroup,
      name,
      audioFiles,
      filters,
      userUuid,
      desc,
    });
  }

  async update(body: EQUpdateDto, eqUuid: string, userUuid: string) {
    const { evaluationUuid, dataGroup, audioFiles, filters, name, open, desc } = body;

    const [evaluation] = await Promise.all([
      this.evaluationModelRepository.findOne({
        where: {
          uuid: evaluationUuid,
        },
      }),
    ]);

    if (!evaluation) {
      throw new BizFailedException(BizFailedCodeEnum.EVALUATION_NOT_EXISTS);
    }

    await this.eqModelRepository.update(
      { uuid: eqUuid, userUuid },
      {
        evaluationUuid,
        dataGroup,
        name,
        audioFiles,
        filters,
        open,
        desc,
      },
    );

    return this.show(eqUuid, userUuid);
  }

  delete(eqUuid: string, userUuid: string) {
    return this.eqModelRepository.delete({ uuid: eqUuid, userUuid });
  }

  // 遍历所有的耳机，所有的调音模式，计算与当前频响曲线的相似度
  async top10(param: EQTop10ListDto) {
    const CHUNK_SIZE = 25;
    const TOP_SIZE = 10;

    const { data } = param;

    let result: Array<{
      name: string;
      evaluationUuid: string;
      dataGroup: string;
      distance: number;
    }> = [];

    const targetScore = scoreCurveVector(data);

    let skip = 0;
    let hasMore = true;

    // 查找所有的耳机
    while (hasMore) {
      const chunk = await this.evaluationDataModelRepository.find({
        skip,
        take: CHUNK_SIZE,
        where: {
          projectUuid: 'cdcf35b6-f37c-4b77-877f-9b8ddbf38c5b',
        },
      });

      if (chunk.length === 0) {
        hasMore = false;
        break;
      }

      let chunkEvaluationSpecifications: EvaluationSpecificationModel[];

      if (param.targetWearingMethod) {
        chunkEvaluationSpecifications = await this.evaluationSpecificationModelRepository.find({
          where: {
            evaluationUuid: In(chunk.map(item => item.evaluationUuid)),
            specificationUuid: '495f4f84-8294-4710-8e43-607cfaf10fa0',
          },
        })
      }

      for (const entity of chunk) {
        if (!entity.data) {
          continue;
        }

        if (param.targetWearingMethod) {
          const currentEvaluationWearingMethodSpecification = chunkEvaluationSpecifications!.find(item => item.evaluationUuid === entity.evaluationUuid);
          if(!currentEvaluationWearingMethodSpecification?.value){
            continue;
          }
          if (
            param.targetWearingMethod === EarBudsWearingMethod.HEADPHONES &&  
            !currentEvaluationWearingMethodSpecification.value.includes('HEADPHONES')
          ) {
            continue;
          }
        }

        const data = csvStrToDataSet(entity.data).slice(1);
        const lastRow = last(data);
        if (!lastRow || lastRow.length < 2) {
          data.pop();
        }

        const score = scoreCurveVector(data);
        const distance = euclideanDistance(targetScore, score);

        result.push({
          name: entity.name,
          dataGroup: entity.group,
          distance,

          evaluationUuid: entity.evaluationUuid,
        });
      }

      skip += CHUNK_SIZE;

      result.sort((a, b) => a.distance - b.distance);
      result = uniqBy(result, (data) => data.evaluationUuid);
      result = result.slice(0, TOP_SIZE);
    }

    if (result.length === 0) {
      return [];
    }

    // 得到结果后，根据evaluationUuid，获取耳机名称
    const qb = this.evaluationService.indexQbSelect();

    qb.andWhere('(evaluation.uuid IN (:...uuids))', {
      uuids: result.map((item) => item.evaluationUuid),
    });

    const evaluations = await qb.getMany();

    const evaluationMap: Record<string, EvaluationModel> = {};
    evaluations.forEach((item) => {
      evaluationMap[item.uuid] = item;
    });

    return result
      .filter((item) => evaluationMap[item.evaluationUuid])
      .map((item) => {
        const { datas, ...restItem } = evaluationMap[item.evaluationUuid];
        return {
          ...restItem,
          dataGroups: this.evaluationDataService.getEvaluationDataGroups(datas),
          hitEvaluationData: {
            distance: item.distance,
            name: item.name,
            dataGroup: item.dataGroup,
          }
        };
      });
  }

  async getFiltersByTempEQ(body: EQGetFiltersByTempEQDto) {
    const points = await this.getPointsByEq({
      evaluationUuid: body.beforeEvaluationUuid,
      dataGroup: body.beforeDataGroup,
      filters: body.beforeFilters as FilterOptions[],
    });
    const filters = await this.getFiltersByPoints({
      evaluationUuid: body.afterEvaluationUuid,
      dataGroup: body.afterDataGroup,
      points,
    });

    return {
      filters,
    }
  }

  async getFiltersBySegmentFreq(body: EQGetFiltersBySegmentFreqDto) {
    const {
      evaluationUuid,
      dataGroup,
      segmentList,
    } = body;

    const evaluationSegmentList = segmentList.filter(segment => segment.evaluationUuid && segment.dataGroup);

    const [
      frequencyEvaluationData,
      wearingMethodSpecification,
      segmentEvaluationDataList,
      segmentEvaluationSpecificationList,
    ] = await Promise.all([
      this.evaluationDataModelRepository.findOne({
        where: {
          evaluationUuid,
          projectUuid: 'cdcf35b6-f37c-4b77-877f-9b8ddbf38c5b',
          group: dataGroup,
        },
      }),
      this.evaluationSpecificationModelRepository.findOne({
        where: {
          evaluationUuid,
          specificationUuid: '495f4f84-8294-4710-8e43-607cfaf10fa0',
        },
      }),
      Promise.all(evaluationSegmentList.map(segment => {
        return this.evaluationDataModelRepository.findOne({
          where: {
            evaluationUuid: segment.evaluationUuid,
            projectUuid: 'cdcf35b6-f37c-4b77-877f-9b8ddbf38c5b',
            group: segment.dataGroup,
          },
        })
      })),
      this.evaluationSpecificationModelRepository.find({
        where: {
          evaluationUuid: In(evaluationSegmentList.map(item=>item.evaluationUuid)),
          specificationUuid: '495f4f84-8294-4710-8e43-607cfaf10fa0',
        },
      })
    ])

    if (!frequencyEvaluationData) {
      throw new BizFailedException(BizFailedCodeEnum.EVALUATION_FREQ_DBA_DATA_NOT_EXISTS);
    }


    // 0 耳塞 1 头戴
    let wearingMethod: 0 | 1 = 0;
    if (wearingMethodSpecification && ["HEADPHONES"].includes(wearingMethodSpecification.value)) {
      wearingMethod = 1;
    }

    return {
      filters: await this.callAutoEqAlgorithm({
        originalPotinsStr: frequencyEvaluationData.data.split('\n').filter((_, index) => index > 0).join('\n'),
        originalWearingMethod: wearingMethod,
        targetList: segmentList.map((item, index) => {
          const common = {
            start: item.start,
            end: item.end,
          }
          if (item.dataSet) {
            return {
              ...common,
              pointsStr: item.dataSet.map(innerArray => innerArray.join(' ')).join('\n'),
              wearingMethod,
            }
          }
          const curSegmentEvaluationData = segmentEvaluationDataList.find(segmentEvaluationData=>segmentEvaluationData?.evaluationUuid === item.evaluationUuid && segmentEvaluationData?.group === item.dataGroup)
          const curSegmentEvaluationSpecification = segmentEvaluationSpecificationList.find(segmentEvaluationSpecification=>segmentEvaluationSpecification.evaluationUuid === item.evaluationUuid)
          let currentWearingMethod: 0 | 1 = 0;
          if (curSegmentEvaluationSpecification && ["HEADPHONES"].includes(curSegmentEvaluationSpecification.value)) {
            currentWearingMethod = 1;
          }

          return {
            start: item.start,
            end: item.end,
            pointsStr: curSegmentEvaluationData!.data.split('\n').filter((_, index) => index > 0).join('\n'),
            wearingMethod: currentWearingMethod,
          }
        }),
      }),
    }
  }


  private async getPointsByEq({
    evaluationUuid,
    dataGroup,
    filters,
  }: {
    evaluationUuid: string;
    dataGroup: string;
    filters: FilterOptions[];
  }) {
    const evaluation = await this.evaluationService.show(evaluationUuid);
    // 过滤出所选择的模式的频响数据
    const freqDbaDataList = evaluation.datas.find(
      (i) => i.project.uuid === 'cdcf35b6-f37c-4b77-877f-9b8ddbf38c5b',
    );
    if (!freqDbaDataList) {
      throw new BizFailedException(
        BizFailedCodeEnum.EVALUATION_FREQ_DBA_DATA_NOT_EXISTS,
      );
    }

    const freqDbaData = freqDbaDataList.children.find(
      (i) => i.group === dataGroup,
    );
    if (!freqDbaData) {
      throw new BizFailedException(
        BizFailedCodeEnum.EVALUATION_FREQ_DBA_DATA_NOT_EXISTS,
      );
    }

    // 转换频响数据
    const sourceData = freqDbaData.dataSet || [];
    const headphonesPoints: Point[] = [];

    for (let j = 1; j < sourceData.length; j++) {
      const item = sourceData[j];
      if (item && item.length > 1) {
        headphonesPoints.push({
          x: Number(item[0]),
          y: Number(item[1]),
        });
      }
    }

    return getFrequencyPoints(
      headphonesPoints,
      filters as FilterOptions[],
    ).map((i) => [i.x, i.y]);
  }

  private async getFiltersByPoints({
    evaluationUuid,
    dataGroup,
    points,
  }: {
    evaluationUuid: string;
    dataGroup: string;
    points: number[][];
  }) {
    const [
      frequencyEvaluationData,
      wearingMethodSpecification,
    ] = await Promise.all([
      this.evaluationDataModelRepository.findOne({
        where: {
          evaluationUuid,
          projectUuid: 'cdcf35b6-f37c-4b77-877f-9b8ddbf38c5b',
          group: dataGroup,
        },
      }),
      this.evaluationSpecificationModelRepository.findOne({
        where: {
          evaluationUuid,
          specificationUuid: '495f4f84-8294-4710-8e43-607cfaf10fa0',
        },
      }),
    ])

    if (!frequencyEvaluationData) {
      throw new BizFailedException(BizFailedCodeEnum.EVALUATION_FREQ_DBA_DATA_NOT_EXISTS);
    }

    // 0 耳塞 1 头戴
    let wearingMethod: 0 | 1 = 0;
    if (wearingMethodSpecification && ["HEADPHONES"].includes(wearingMethodSpecification.value)) {
      wearingMethod = 1;
    }

    return this.callAutoEqAlgorithm({
      originalPotinsStr: frequencyEvaluationData.data.split('\n').filter((_, index) => index > 0).join('\n'),
      originalWearingMethod: wearingMethod,
      targetList: [
        {
          start: 20,
          end: 20000,
          wearingMethod,
          pointsStr: points.map(innerArray => innerArray.join(' ')).join('\n'),
        }
      ],
    })

  }

  private async callAutoEqAlgorithm({
    originalPotinsStr,
    originalWearingMethod,
    targetList,
  }: {
    originalPotinsStr: string;
    originalWearingMethod: number;
    targetList: {
      pointsStr: string;
      wearingMethod: number;
      start: number;
      end: number;
    }[]
  }) {
    const taskId = `${dayjs().format('YYYYMMDDHHmmss')}-${customAlphabet('0123456789', 6)()}`

    const tempFileDir = path.join(
      this.tempFilePath,
      'eq-filter',
      taskId,
    );

    fsExtra.ensureDirSync(tempFileDir);

    const originalPath = path.join(tempFileDir, 'original.txt');
    const outputPathPrefix = path.join(tempFileDir, 'result');
    const filterNum = 10;

    const currentLog = (
      message: string,
      type: 'log' | 'warn' | 'error' = 'log',
    ) => {
      this.loggerService[type](`获取滤波器 ${taskId}：${message}`);
    };

    currentLog('写入原始频响文件');
    fsExtra.writeFileSync(originalPath, originalPotinsStr);
    currentLog(`写入原始频响文件完成：${originalPath}`);

    currentLog('写入目标频响文件');
    targetList.forEach((target, index) => {
      const targetPath = path.join(tempFileDir, `target-${index}.txt`);
      fsExtra.writeFileSync(targetPath, target.pointsStr);
      currentLog(`写入目标频响文件完成：${targetPath}`);
    })

    const command = {
      command: `./build/test_autoeqprocessor_sh ${originalPath} ${originalWearingMethod} ${filterNum} ${outputPathPrefix} ${targetList.map((target, index) => {
        return `${target.start} ${target.end} ${path.join(tempFileDir, `target-${index}.txt`)} ${target.wearingMethod}`;
      }).join(' ')}`,
      cwd: '/home/ntz/autoeq',
    }

    const generate = () => {
      return new Promise<string>((resolve, reject) => {
        currentLog(`在目录 ${command.cwd} 下执行命令：${command.command}`);

        if (this.useConv) {
          const outputPath = `${outputPathPrefix}_filter_parameters.txt`;

          exec(
            command.command,
            {
              encoding: 'utf8',
              cwd: command.cwd,
            },
            (error, stdout, stderr) => {
              if (error) {
                currentLog(
                  `Error executing command: ${error.message}`,
                  'error',
                );
                return reject(error);
              }

              if (stderr) {
                currentLog(`Error in processing: ${stderr}`, 'error');
                return reject(error);
              }

              currentLog(`Command output: ${stdout}`); // 调试信息

              currentLog(`生成完成：${outputPath}`);
              resolve(outputPath);
            },
          );
        } else {
          const outputPath = path.join(this.tempFilePath, 'eq-filter/result_filter_parameters.txt');

          currentLog(`生成完成：${outputPath}`);
          resolve(outputPath);
        }
      });
    }

    const generatePath = await generate();

    const generateFile = fsExtra.readFileSync(generatePath, 'utf-8').trim();

    const generateRows = generateFile.split('\n');

    const filterResult: FilterOptions[] = [];

    generateRows.forEach((row, index) => {
      if (index > 1) {
        const columns = row.split(';');
        filterResult.push({
          filterType: 'peaking',
          frequency: Number(columns[1]),
          q: Number(columns[2]),
          gain: Number(columns[3]),
        })
      }
    })

    fsExtra.remove(tempFileDir)

    return filterResult;
  }


}

export interface Point {
  x: number;
  y: number;
  label?: string;
  data?: any; // extra data
}
